"""
visualizer/app.py
该文件是ChatAD项目的Web后端服务，基于Python Flask框架构建。
它的核心职责包括：
1. 提供静态文件服务，承载前端UI（HTML, CSS, JS）。
2. 提供一系列API端点，用于处理前端的请求。
3. 作为"总调度中心"，接收前端任务，调用底层的AI核心逻辑（run.py）。
4. 与第三方AI服务（如Aippt）进行后端到后端的安全通信。
5. 处理并解析AI任务产生的日志文件，将非结构化数据转化为结构化的JSON，提供给前端展示。
6. 提供项目产出物的打包下载功能。
"""
import logging
import requests
import os
from flask import Flask, send_from_directory, request, jsonify, send_file
import argparse
import subprocess
import io
import sys
import datetime
import time
import glob
import re
import shutil
import json
import zipfile
# set $env:PYTHONIOENCODING="UTF-8"
os.environ["PYTHONIOENCODING"] = "UTF-8"
# 配置日志
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs")
os.makedirs(log_dir, exist_ok=True)

log_file = os.path.join(log_dir, f"flask_app_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.log")
logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s %(levelname)s] %(message)s',
    datefmt='%Y-%d-%m %H:%M:%S',
    handlers=[
        logging.FileHandler(log_file, encoding='utf-8'),
        logging.StreamHandler(sys.stdout)
    ]
)

logger = logging.getLogger(__name__)

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf8')
app = Flask(__name__, static_folder='static')
app.logger.setLevel(logging.ERROR)
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
messages = []

# 用于存储当前使用的端口
CURRENT_PORT = 8946  # 设置默认端口为8946
SERVER_RUNNING = False  # 标记服务器是否运行

# PPTist 后端配置
AIPPT_BACKEND_BASE_URL = "http://127.0.0.1:5000" # 确保PPTist后端在此运行

@app.route("/")
def index():
    """提供主页面 index.html"""
    return send_from_directory("static", "index.html")

@app.route("/chain_visualizer")
def chain_visualizer():
    """提供ChatChain可视化页面"""
    return send_from_directory("static", "chain_visualizer.html")

@app.route("/replay")
def replay():
    """提供对话重放页面"""
    return send_from_directory("static", "replay.html")

@app.route("/get_messages")
def get_messages():
    """获取实时聊天消息（保留功能）"""
    return jsonify(messages)

@app.route("/send_message", methods=["POST"])
def send_message():
    """接收并广播聊天消息（保留功能）"""
    data = request.get_json()
    role = data.get("role")
    text = data.get("text")

    avatarUrl = find_avatar_url(role)

    message = {"role": role, "text": text, "avatarUrl": avatarUrl}
    messages.append(message)
    return jsonify(message)

@app.route("/send_task", methods=["POST"])
def send_task():
    """
    【核心】接收前端发起的AI任务，并调用底层脚本执行。
    这是整个应用的起点。
    """
    data = request.get_json()
    task_prompt = data.get("role") # 'role'字段实际上是任务描述
    project_name = data.get("text") # 'text'字段实际上是项目名称
    logger.info(f"收到任务: task='{task_prompt}', name='{project_name}'")

    try:
        alt_result = None
        # 获取ChatAD项目的根目录
        current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        logger.info(f"ChatAD 根目录: {current_dir}")

        timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")

        # 为子进程准备环境变量，特别是API密钥
        env_vars = os.environ.copy()
        env_vars["OPENAI_API_KEY"] = "sk-a37e2faac6794a6cba2c363a5d24888d" # 主API密钥
        env_vars["DEFAULT_AI_MODEL"] = "deepseek-chat"
        env_vars["AI_PROVIDER"] = "openai"
        env_vars["BASE_URL"] = "https://api.deepseek.com/"

        # 构建调用run.py的命令
        python_executable = sys.executable
        command_args = [
            python_executable,
            os.path.join(current_dir, "run.py"),
            "--config", "Default",
            "--org", "DefaultOrganization",
            "--task", task_prompt, # 使用 POST 请求中的 role 作为 task prompt
            "--name", project_name, # 使用 POST 请求中的 text 作为 name
            "--model", "DEFAULT_MODEL"
        ]
        logger.info(f"准备执行命令: {' '.join(command_args)}")
        logger.info(f"使用环境变量: OPENAI_API_KEY=..., DEFAULT_AI_MODEL={env_vars['DEFAULT_AI_MODEL']}, AI_PROVIDER={env_vars['AI_PROVIDER']}, BASE_URL={env_vars['BASE_URL']}")

        # 执行命令
        logger.info("开始执行任务...")
        result = subprocess.run(
            command_args,
            capture_output=True, # 捕获标准输出和标准错误
            text=True,
            check=False, # 不在执行失败时抛出异常，而是手动检查返回码
            timeout=1800, # 设置超时时间（30分钟）
            cwd=current_dir,
            env=env_vars,
            encoding='utf-8',
            errors='ignore'
        )

        # 记录输出
        logger.info(f"命令执行结束，返回码: {result.returncode}")
        log_stdout_path = os.path.join(log_dir, f"stdout_{timestamp}.log")
        log_stderr_path = os.path.join(log_dir, f"stderr_{timestamp}.log")

        if result.stdout:
            logger.info(f"标准输出 (部分): {result.stdout[:500]}...")
            with open(log_stdout_path, "w", encoding="utf-8") as f:
                f.write(result.stdout)
            logger.info(f"完整标准输出已保存至: {log_stdout_path}")

        if result.stderr:
            logger.error(f"错误输出 (部分): {result.stderr[:500]}...")
            with open(log_stderr_path, "w", encoding="utf-8") as f:
                f.write(result.stderr)
            logger.error(f"完整错误输出已保存至: {log_stderr_path}")

        # 检查是否有特定错误 (例如 API key 问题)
        if result.returncode != 0 and "400 Bad Request" in (result.stderr or ""):
            logger.error("API调用出错: 400 Bad Request，尝试使用备用API密钥...")

            # 更新环境变量为备用密钥
            alt_env_vars = env_vars.copy()
            alt_env_vars["OPENAI_API_KEY"] = "sk-0bd0ca24a0c5f6a3afe5c2280f868aa1" # 备用密钥
            logger.info(f"使用备用环境变量: OPENAI_API_KEY=..., ...")


            # 使用备用密钥重新执行命令
            alt_result = subprocess.run(
                command_args, # 命令参数不变
                capture_output=True,
                text=True,
                check=False,
                timeout=1800,
                cwd=current_dir,
                env=alt_env_vars, # 使用备用环境变量
                encoding='utf-8',
                errors='ignore'
            )

            # 记录备用尝试的输出
            logger.info(f"备用密钥尝试结束，返回码: {alt_result.returncode}")
            alt_log_stdout_path = os.path.join(log_dir, f"alt_stdout_{timestamp}.log")
            alt_log_stderr_path = os.path.join(log_dir, f"alt_stderr_{timestamp}.log")

            if alt_result.stdout:
                logger.info(f"备用尝试 - 标准输出 (部分): {alt_result.stdout[:500]}...")
                with open(alt_log_stdout_path, "w", encoding="utf-8") as f:
                    f.write(alt_result.stdout)
                logger.info(f"备用尝试 - 完整标准输出已保存至: {alt_log_stdout_path}")

            if alt_result.stderr:
                logger.error(f"备用尝试 - 错误输出 (部分): {alt_result.stderr[:500]}...")
                with open(alt_log_stderr_path, "w", encoding="utf-8") as f:
                    f.write(alt_result.stderr)
                logger.error(f"备用尝试 - 完整错误输出已保存至: {alt_log_stderr_path}")

            if alt_result.returncode == 0:
                logger.info("备用API密钥成功!")
            else:
                logger.error(f"备用API密钥也失败了。")

        # 从stdout中解析真实的项目路径
        output = result.stdout or ""
        if alt_result and alt_result.stdout:
            output += alt_result.stdout

        project_folder_name = None
        for line in output.splitlines():
            if line.startswith("CHATDEV_PROJECT_PATH:"):
                project_folder_name = line.split(":", 1)[1].strip()
                logger.info(f"成功解析到项目文件夹名称: {project_folder_name}")
                break
        
        if not project_folder_name:
            logger.error("无法从脚本输出中解析出项目文件夹名称。")
            # 即使无法解析，也可能成功了，但下载功能会失效
            return jsonify({"status": "success", "message": "任务已执行，但无法确认项目文件夹名称，下载功能可能不可用。"}), 200

    except subprocess.TimeoutExpired:
        logger.error("命令执行超时")
        return jsonify({"status": "error", "message": "任务执行超时。"}), 500
    except Exception as e:
        logger.exception(f"执行任务时发生意外错误: {str(e)}")
        return jsonify({"status": "error", "message": f"执行任务时发生意外错误: {str(e)}"}), 500

    # 任务成功发送后，提示用户可以进行后续操作
    return jsonify({"status": "success", "message": "任务已成功执行。您现在可以进行后续操作。", "task_name_created": project_folder_name})

@app.route('/download/<task_name>')
def download_project(task_name):
    """
    下载指定项目文件夹。
    :param task_name: 任务名称，也是文件夹名称。
    """
    try:
        # 在服务器的 'WareHouse' 目录下定位项目文件夹
        base_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "WareHouse")
        project_full_path = os.path.join(base_dir, task_name)

        if not os.path.isdir(project_full_path):
            return "Project not found", 404

        # 在内存中创建一个二进制IO流，作为临时的zip文件容器
        memory_file = io.BytesIO()
        # 使用zipfile库，将项目文件夹中的所有内容写入到内存流中
        with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf:
            for root, dirs, files in os.walk(project_full_path):
                for file in files:
                    file_path = os.path.join(root, file)
                    # 计算文件在zip包内的相对路径，避免暴露服务器的绝对路径结构
                    arcname = os.path.relpath(file_path, start=project_full_path)
                    zf.write(file_path, arcname)
        # 将内存流的指针移到开头，准备读取数据
        memory_file.seek(0)

        # 使用Flask的send_file功能，将内存中的zip数据流作为附件发送给浏览器
        return send_file(
            memory_file,
            download_name=f'{task_name}.zip',
            as_attachment=True
        )
    except Exception as e:
        return str(e), 500

def find_avatar_url(role):
    """根据角色名称，找到对应的头像图片URL。（保留功能）"""
    role = role.replace(" ", "%20")
    avatar_filename = f"avatars/{role}.png"
    avatar_url = f"/static/{avatar_filename}"
    return avatar_url

# find_latest_stdout_log 和 extract_ppt_generation_results 保留用于未来的使用（DocMee集成）
def find_latest_stdout_log(directory):
    pattern = os.path.join(directory, "stdout*.log") # Assuming this is the correct pattern for logs from run.py
    # If run.py logs are named differently, e.g., based on task_name directly, adjust this.
    # For example: pattern = os.path.join(directory, f"{task_name_pattern_if_known}*.log")
    log_files = glob.glob(pattern)
    if not log_files:
        logger.info(f"在 {directory} 中没有找到匹配 'stdout*.log' 的日志文件")
        return None
    # 通过比较文件的最后修改时间来找到最新的文件
    latest_file = max(log_files, key=os.path.getmtime)
    logger.info(f"找到最新的stdout日志文件: {latest_file}")
    return latest_file

def extract_ppt_generation_results(file_path):
    """
    从指定的日志文件中提取 **[PPT Generation Results]**: 标记后的所有内容。
    这是一个简单的单标记提取器。
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        marker = "**[PPT Generation Results]**:"
        match_index = content.find(marker)
        
        if match_index != -1:
            start_extraction = match_index + len(marker)
            extracted_content = content[start_extraction:].strip()
            if extracted_content:
                logger.info(f"成功从第一个 '{marker}' 标记后提取到内容 (文件: {file_path}, 长度: {len(extracted_content)}).")
                return extracted_content
            else:
                logger.warning(f"在文件 {file_path} 中，第一个 '{marker}' 标记后没有有效内容。")
                return None
        else:
            # Marker not found
            logger.warning(f"在文件 {file_path} 中未找到任何 '{marker}' 标记。")
            return None

    except FileNotFoundError:
        logger.error(f"提取内容时文件未找到: {file_path}")
        return None
    except Exception as e:
        logger.error(f"提取内容 '{marker}' 时发生未知错误 ({file_path}): {str(e)}")
        return None

def send_msg(role, text):
    """向前端发送实时消息（保留功能）"""
    try:
        # 总是尝试发送消息，但添加超时设置
        port = CURRENT_PORT
        data = {"role": role, "text": text}
        response = requests.post(f"http://127.0.0.1:{port}/send_message", json=data, timeout=0.5)
        logger.info(f"发送消息: {role}, {text[:30]}...")
    except requests.exceptions.Timeout:
        logger.warning(f"发送消息超时: {role}")
    except requests.exceptions.ConnectionError:
        logger.warning(f"连接失败，消息未发送: {role}")
    except Exception as e:
        logger.error(f"发送消息失败: {str(e)}")
        logging.info("flask app.py did not start for online log")

@app.route("/api/get_latest_ppt_text_content", methods=["GET"])
def get_latest_ppt_text_content():
    """
    【核心】内容提取API：获取最新的研讨会结论文本，用于填充PPT编辑器。
    """
    try:
        # 1. 找到最新的日志文件
        latest_log_file = find_latest_stdout_log(log_dir)
        if not latest_log_file:
            return jsonify({"error": "未找到合适的日志文件以提取PPT内容。"}), 404
        
        # 2. 从日志文件中提取PPT内容
        ppt_content = extract_ppt_generation_results(latest_log_file)
        
        if ppt_content is None:
            return jsonify({"error": "无法从最新的日志中提取PPT内容。", "使用的日志文件": os.path.basename(latest_log_file)}), 404
        
        # 3. 将提取的内容以JSON格式返回给前端
        return jsonify({"ppt_content": ppt_content, "source_log_file": os.path.basename(latest_log_file)})

    except Exception as e:
        logger.error(f"Error in /api/get_latest_ppt_text_content: {e}", exc_info=True)
        return jsonify({"error": "在获取PPT内容时发生意外错误", "details": str(e)}), 500

@app.route("/api/generate_ppt_via_aippt", methods=["POST"])
def generate_ppt_via_aippt():
    """
    【核心】第三方服务集成API：调用外部Aippt服务来智能生成PPT。
    这是一个后端到后端的安全调用。
    """
    try:
        data = request.get_json()
        if not data:
            return jsonify({"error": "请求体必须为JSON"}), 400

        subject = data.get("subject")
        language = data.get("language", "中文") # 如果未提供，则默认使用中文
        ai_model = data.get("model") # 可选的模型覆盖

        if not subject:
            return jsonify({"error": "请求体中缺少'subject'"}), 400

        logger.info(f"收到请求，通过AIPPT后端生成PPT，主题: {subject}, 语言: {language}")

        outline_payload = {
            "content": subject,
            "language": language,
            "stream": False # 请求非流式大纲，这里简化处理
        }
        if ai_model:
            outline_payload["model"] = ai_model
        
        outline_url = f"{AIPPT_BACKEND_BASE_URL}/tools/aippt_outline"
        logger.info(f"调用AIPPT大纲API: {outline_url} 传入参数: {outline_payload}")
        
        outline_response = requests.post(outline_url, json=outline_payload, timeout=300) # 5 min timeout
        outline_response.raise_for_status() # 为HTTP错误抛出异常
        
        ppt_outline_text = outline_response.text # 假设非流式返回完整文本
        logger.info(f"收到PPT大纲 (前500字符): {ppt_outline_text[:500]}")

        content_payload = {
            "content": ppt_outline_text,
            "language": language,
            "stream": True # PPTist后端的/aippt流式返回JSON行
        }
        if ai_model:
            content_payload["model"] = ai_model

        content_url = f"{AIPPT_BACKEND_BASE_URL}/tools/aippt"
        logger.info(f"调用AIPPT内容API: {content_url} 传入大纲")

        ppt_pages_json_list = []
        # 使用stream=True与requests处理流式响应
        with requests.post(content_url, json=content_payload, stream=True, timeout=600) as content_response: # 10 min timeout
            content_response.raise_for_status()
            for line in content_response.iter_lines():
                if line:
                    try:
                        # 每一行都应是代表幻灯片的 JSON 对象，将字节解码为字符串
                        decoded_line = line.decode('utf-8').strip()
                        if decoded_line: # 确保条形图后不是空行
                            logger.debug(f"Received content line: {decoded_line}")
                            ppt_page_json = json.loads(decoded_line)
                            ppt_pages_json_list.append(ppt_page_json)
                    except json.JSONDecodeError as je:
                        logger.error(f"从AIPPT内容流中解码JSON行失败: '{line.decode('utf-8', errors='ignore')}' - 错误: {je}")
                    except Exception as e_line:
                        logger.error(f"处理AIPPT内容流中的行时发生错误: {e_line}")
        
        logger.info(f"成功从AIPPT后端收集到 {len(ppt_pages_json_list)} 页。")
        
        return jsonify(ppt_pages_json_list)

    except requests.exceptions.RequestException as re:
        logger.error(f"AIPPT后端请求失败: {re}")
        error_message = f"无法连接或与AIPPT后端通信失败: {str(re)}"
        if re.response is not None:
            try:
                backend_error = re.response.json()
                error_message = backend_error.get("detail", error_message) if isinstance(backend_error, dict) else str(backend_error)
            except ValueError: # 不是JSON响应
                 error_message = re.response.text[:200] # 显示回复的前 200 个字符
        return jsonify({"error": "AIPPT后端通信错误", "details": error_message}), 502
    except Exception as e:
        logger.error(f"在/api/generate_ppt_via_aippt中发生错误: {e}", exc_info=True)
        return jsonify({"error": "发生意外错误", "details": str(e)}), 500

def extract_section_from_log(log_content, end_marker, start_marker="**[Seminar Conclusion]**:"):
    """
    【核心】通用的内容提取函数，能够从大段文本中提取位于两个标记之间的最新部分。
    这是一个更健壮、更通用的提取器。
    """
    try:
        # 使用rfind从后往前查找，确保找到的是最新出现的标记
        last_end_marker_idx = log_content.rfind(end_marker)
        if last_end_marker_idx == -1:
            logger.warning(f"在日志中未找到结束标记 '{end_marker}'。")
            return None

        # 在结束标记之前的部分，再次从后往前查找开始标记
        content_before_end_marker = log_content[:last_end_marker_idx]
        last_start_marker_idx = content_before_end_marker.rfind(start_marker)
        
        if last_start_marker_idx == -1:
            logger.warning(f"在结束标记 '{end_marker}' 之前未找到开始标记 '{start_marker}'。")
            return None
            
        # Ensure start_marker is indeed before end_marker in the context of rfind
        if last_start_marker_idx > last_end_marker_idx: # Should not happen with current logic but defensive
            logger.warning(f"逻辑错误: 在以 '{end_marker}' 结尾的部分中，开始标记出现在结束标记之后。")
            return None

        # Extract text between the end of start_marker and the beginning of end_marker
        text_start_idx = last_start_marker_idx + len(start_marker)
        section_text = log_content[text_start_idx:last_end_marker_idx].strip()
        
        # Remove any leading log timestamps if present in the extracted text
        section_text = re.sub(r"^\s*\[\d{{4}}-\d{{2}}-\d{{2}}\s+\d{{2}}:\d{{2}}:\d{{2}}\s+INFO\s*\]\s*", "", section_text, flags=re.MULTILINE).strip()
        
        # Remove "<好的>" if it's the first part of the content
        if section_text.startswith("<好的>"):
            section_text = section_text[len("<好的>"):].strip()
            
        return section_text
    except Exception as e:
        logger.error(f"提取 '{start_marker}' 和 '{end_marker}' 之间的部分时发生错误: {e}")
        return None

def parse_shooting_suggestions_to_json(text_block):
    """
    【核心】ETL函数：将"广告拍摄建议"的非结构化文本块，转换为结构化的JSON对象数组。
    """
    if not text_block:
        return []

    suggestions = []
    
    # 强大的停止标记，以防止解析器读入日志或其他不相关部分
    stop_marker_pattern = re.compile(
        r"^\s*(\*\*\[(投放建议|Seminar Conclusion|chatting)\]\*\*:|\||CHATDEV_PROJECT_PATH|\[\d{4}-\d{2}-\d{2})"
    )

    current_details = []
    last_category = None

    # 采用逐行解析，以提高健壮性
    for line in text_block.splitlines():
        line = line.strip()
        if not line:
            continue

        # 如果遇到停止标记，则立即中断解析
        if stop_marker_pattern.search(line):
            logger.info(f"广告拍摄建议解析在遇到停止标记时中断: '{line}'")
            break

        # 用于查找作为类别的行 (例如, "1. 标题:", "标题:")
        match_category = re.match(r"^(?:\d+\.\s*)?([^：\n]+?[:：])\s*$", line)
        if match_category:
            # 如果找到新类别，则保存前一个类别及其所有详细信息
            if last_category and current_details:
                suggestions.append({
                    "category": last_category,
                    "details": "\n".join(current_details).strip()
                })
            
            # 清理并设置新类别
            last_category = match_category.group(1).strip().rstrip(':：')
            current_details = []  # 为新类别重置详细信息列表
        elif last_category:
            # 此行为当前类别的详细信息
            current_details.append(line)
    
    # 在循环结束后，追加最后一个收集到的类别
    if last_category and current_details:
        suggestions.append({
            "category": last_category,
            "details": "\n".join(current_details).strip()
        })
    
    # 针对没有"类别:"结构的文本块的回退机制
    if not suggestions and text_block:
        clean_lines = []
        for line in text_block.splitlines():
            if stop_marker_pattern.search(line):
                break
            clean_lines.append(line)
        clean_text_block = "\n".join(clean_lines).strip()
        if clean_text_block:
            suggestions.append({"category": "建议详情", "details": clean_text_block})

    logger.info(f"解析后的广告拍摄建议: {suggestions}")
    return suggestions


def parse_placement_recommendations_to_json(text_block):
    """
    【核心】ETL函数：将"广告投放建议书"的复杂文本块，解析为包含多层嵌套的结构化JSON。
    """
    if not text_block:
        return {}
    
    recommendations = {"schemes": []} # 移除general_sections作为顶级，它们成为方案如果结构化
    
    raw_lines = text_block.splitlines()
    if not raw_lines:
        logger.info("Placement recommendations text_block is empty after splitlines.")
        return {}

    cleaned_lines = []
    # 移除已知的介绍性和结束行，并转换 \\n 为 \n
    skip_phrases = [
        "基于市场分析数据，我为新奶茶产品制定以下两套广告投放策略方案：",
        "（方案制定完毕）",
        "(方案制定完毕)" # 处理这两种括号类型
    ]
    for raw_line in raw_lines:
        stripped_line = raw_line.strip()
        
        # 跳过特定短语
        is_skip_phrase = False
        for phrase in skip_phrases:
            if stripped_line == phrase: # 精确匹配这些完整行
                logger.info(f"Skipping specific phrase: {stripped_line}")
                is_skip_phrase = True
                break
        if is_skip_phrase:
            continue

        if stripped_line: # 添加非空行
            # 将文字 '\\n' 转换为实际的换行符 '\n'
            processed_line = stripped_line.replace('\\\\n', '\\n')
            cleaned_lines.append(processed_line)

    lines = cleaned_lines
    if not lines:
        logger.info("Placement recommendations lines are empty after cleaning.")
        return {}
        
    logger.info(f"Starting to parse placement recommendations. Number of lines after cleaning: {len(lines)}.")

    current_scheme_title = None
    current_scheme_items = []
    current_scheme_sub_item_title = None
    current_scheme_sub_item_details_lines = []

    def save_pending_sub_item():
        nonlocal current_scheme_sub_item_title, current_scheme_sub_item_details_lines, current_scheme_items
        if current_scheme_sub_item_title and current_scheme_sub_item_details_lines:
            logger.info(f"    Saving sub-item: '{current_scheme_sub_item_title}' with {len(current_scheme_sub_item_details_lines)} lines of details.")
            current_scheme_items.append({
                "sub_title": current_scheme_sub_item_title,
                "details": '\\n'.join(current_scheme_sub_item_details_lines)
            })
        elif current_scheme_sub_item_title: # 标题在那里，但没有细节
             logger.info(f"    Saving sub-item (no details): '{current_scheme_sub_item_title}'")
             current_scheme_items.append({
                "sub_title": current_scheme_sub_item_title,
                "details": ""
            })
        current_scheme_sub_item_title = None
        current_scheme_sub_item_details_lines = []

    def save_pending_scheme():
        nonlocal current_scheme_title, current_scheme_items
        save_pending_sub_item() 
        if current_scheme_title and current_scheme_items:
            logger.info(f"  Saving scheme: '{current_scheme_title}' with {len(current_scheme_items)} items.")
            recommendations["schemes"].append({"title": current_scheme_title, "items": current_scheme_items})
        elif current_scheme_title: # 方案标题被识别但未解析任何项目
            logger.info(f"  Saving scheme (no items): '{current_scheme_title}'") # 记录但仍然添加它。
            # 对于像 "数据深化方向" 这样的部分，如果没有结构化的子项，我们可以将它们的
            # 内容作为单个项目处理。这种情况在将行附加到 current_scheme_sub_item_details_lines 时得到处理。
            # 在创建隐式子项时检查 `if not current_scheme_items` 是有帮助的。
            recommendations["schemes"].append({"title": current_scheme_title, "items": []})
        current_scheme_title = None
        current_scheme_items = []

    # 包括 "方案X:", "补充建议:", "数据深化方向:", 等在内的顶级方案标题的正则表达式
    scheme_title_pattern = r"^(【[^】]+】.*|方案(?:[一二三四五六七八九十]|十[一二三四五六七八九])[:：].*|补充建议：|数据深化方向：|风险控制措施：|建立三套预警机制：)$"
    # 用于停止解析的停止标记的正则表达式，当遇到日志或不相关的部分时立即停止解析。
    stop_marker_pattern = re.compile(r"^\s*\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}|\*\*\[chatting|model_type|CHATDEV_PROJECT_PATH|<好的>")


    for i, line in enumerate(lines):
        logger.debug(f"Processing line {i+1}/{len(lines)}: '{line}'")

        # 首先，检查是否存在停止标记以立即停止解析。
        if stop_marker_pattern.match(line):
            logger.info(f"Parsing halted due to stop marker on line: '{line[:70]}...'")
            break # 立即退出循环。

        scheme_match = re.match(scheme_title_pattern, line)
        if scheme_match:
            matched_title = scheme_match.group(1).strip()
            logger.info(f"Line '{line}' matched SCHEME pattern with title: '{matched_title}'.")
            save_pending_scheme() # 保存之前的方案
            
            current_scheme_title = matched_title
            current_scheme_items = []
            current_scheme_sub_item_title = None
            current_scheme_sub_item_details_lines = []
            logger.info(f"  IDENTIFIED NEW SCHEME: {current_scheme_title}")
            continue

        if current_scheme_title: # 如果我们正在一个方案中
            # 用于子项标题的正则表达式 (例如, "1. 渠道组合：" 或 "渠道组合：")
            # 使用 \n 而不是 \\n 修正正则表达式
            sub_item_title_match = re.match(r"^(?:\d+\.\s*)?([^：\n]+?：)$", line)
            if sub_item_title_match:
                save_pending_sub_item() 
                current_scheme_sub_item_title = sub_item_title_match.group(1).strip()
                current_scheme_sub_item_details_lines = [] 
                logger.info(f"    Identified new sub-item for '{current_scheme_title}': '{current_scheme_sub_item_title}'")
            elif current_scheme_sub_item_title: 
                logger.debug(f"    Adding detail to sub-item '{current_scheme_sub_item_title}': '{line}'")
                current_scheme_sub_item_details_lines.append(line)
            else: 
                # 这种逻辑处理像 "数据深化方向" 这样的部分，它们有一个标题
                # 但它们的内容在下一行没有正式的子项标题。
                if not current_scheme_items and not current_scheme_sub_item_title :
                    current_scheme_sub_item_title = "详细内容" # 默认子项标题
                    logger.info(f"    Implicitly starting sub-item '{current_scheme_sub_item_title}' for scheme '{current_scheme_title}' with line: {line}")
                    current_scheme_sub_item_details_lines.append(line)
                else:
                     logger.warning(f"    Line '{line}' in scheme '{current_scheme_title}' is unattached. No active sub-item. IGNORING.")
            continue
            
        logger.warning(f"Line '{line}' was not handled by any parsing rule and not in a scheme.")

    save_pending_scheme() # 保存任何最后待处理的方案

    logger.info(f"FINAL Parsed placement recommendations: {json.dumps(recommendations, ensure_ascii=False, indent=2)}")
    return recommendations


@app.route("/api/get_latest_suggestions", methods=["GET"])
def get_latest_suggestions_api():
    """
    【核心】内容提取API：获取广告拍摄建议案和投放建议书，并以JSON格式提供。
    """
    try:
        latest_log_file = find_latest_stdout_log(log_dir)
        if not latest_log_file:
            return jsonify({"error": "No stdout log file found."}), 404

        with open(latest_log_file, 'r', encoding='utf-8') as f:
            log_content = f.read()

        # 1. 使用通用的提取函数，从日志中提取出两段不同的文本块
        shooting_suggestions_text = extract_section_from_log(
            log_content,
            end_marker="**[Advertising Shooting Suggestions]**:"
        )
        logger.info(f"DEBUG: Raw extracted shooting_suggestions_text: --------\\n{shooting_suggestions_text[:500] if shooting_suggestions_text else 'None'}...\\n--------")

        placement_recommendations_text = extract_section_from_log(
            log_content,
            end_marker="**[投放建议]**:"
        )
        logger.info(f"DEBUG: Raw extracted placement_recommendations_text: --------\\n{placement_recommendations_text[:500] if placement_recommendations_text else 'None'}...\\n--------")
        shooting_json = parse_shooting_suggestions_to_json(shooting_suggestions_text)
        placement_json = parse_placement_recommendations_to_json(placement_recommendations_text)
        
        if not shooting_json and not placement_json:
            return jsonify({
                "error": "无法从最新的日志中提取或解析任何建议内容。", 
                "log_file_used": os.path.basename(latest_log_file)
            }), 404

        # 2. 将两个结构化数据聚合后，返回给前端
        return jsonify({
            "shooting_suggestions": shooting_json,
            "placement_recommendations": placement_json,
            "source_log_file": os.path.basename(latest_log_file)
        })

    except Exception as e:
        logger.error(f"在/api/get_latest_suggestions中发生错误: {e}", exc_info=True)
        return jsonify({"error": "服务器上发生意外错误", "details": str(e)}), 500

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='argparse')
    parser.add_argument('--port', type=int, default=8946, help="port")
    args = parser.parse_args()
    
    # 全局设置当前端口
    CURRENT_PORT = args.port
    logger.info(f"使用端口: {CURRENT_PORT}")
    
    print(f"请访问 http://127.0.0.1:{CURRENT_PORT}/ 查看前端显示页面。")
    print(f"如果端口冲突，请修改端口参数（例如: python app.py --port 8012）")
    sys.stdout.flush()  # 立即刷新输出缓冲区

    try:
        app.run(host='0.0.0.0', debug=False, port=CURRENT_PORT)
    except KeyboardInterrupt:
        print("\n服务器已停止")
